package jxtend.processor.eclipse;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import jxtend.processor.eclipse.utils.ASTNodeKind;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.reflect.ReflectField;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.codeassist.complete.CompletionOnQualifiedNameReference;
import org.eclipse.jdt.internal.codeassist.select.SelectionOnQualifiedNameReference;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.NameReference;
import org.eclipse.jdt.internal.compiler.ast.QualifiedNameReference;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.eval.CodeSnippetQualifiedNameReference;
import org.eclipse.jdt.internal.eval.EvaluationContext;

@SuppressWarnings("restriction")
/* default */ final class NameReferenceUtils {

	public static Map<NameReference, NameReference> backupNameReferences(CompilationUnitDeclaration compilationUnit) {
		final Map<NameReference, NameReference> result = new HashMap<NameReference, NameReference>();

		compilationUnit.traverse(new NameReferenceVisitor() {
			@Override
			public void visitNameReference(NameReference nameReference) {
				result.put(nameReference, cloneNameReference(nameReference));
			}

		}, compilationUnit.scope);

		
		return result;
	}

	private static NameReference cloneNameReference(NameReference nameReference) {
		ASTNodeKind kind = ASTNodeKind.getEnum(nameReference);

		NameReference result;
		if (nameReference instanceof QualifiedNameReference) {
			QualifiedNameReference qualifiedNameReference = (QualifiedNameReference) nameReference;
			long[] positions = qualifiedNameReference.sourcePositions;
			char[][] tokens = qualifiedNameReference.tokens;

			if (kind == ASTNodeKind.SELECTION || kind == ASTNodeKind.COMPLETION) {
				if (kind == ASTNodeKind.SELECTION) {
					char[][] previousIdentifiers = (tokens.length > 1)
							? CharOperation.subarray(tokens, 0, tokens.length - 2) : new char[0][];
					char[] selectionIdentifier = (tokens.length > 0) ? tokens[tokens.length - 1] : new char[0];

					result = new SelectionOnQualifiedNameReference(previousIdentifiers, selectionIdentifier, positions);
				} else {
					assert kind == ASTNodeKind.COMPLETION;

					result = new CompletionOnQualifiedNameReference(tokens,
							((CompletionOnQualifiedNameReference) nameReference).completionIdentifier, positions, true);
//							((CompletionOnQualifiedNameReference) nameReference).isInsideAnnotationAttribute);
					((CompletionOnQualifiedNameReference) result).completionIdentifier =
						((CompletionOnQualifiedNameReference) nameReference).completionIdentifier;
				}
			} else {
				assert kind == ASTNodeKind.CODE_SNIPPET || kind == ASTNodeKind.NORMAL;

				char[][] sources = qualifiedNameReference.tokens;

				if (kind == ASTNodeKind.CODE_SNIPPET) {
					EvaluationContext evaluationContext = ReflectField.getField(
								CodeSnippetQualifiedNameReference.class, EvaluationContext.class, "evaluationContext")
							.get((CodeSnippetQualifiedNameReference) nameReference);

					result = new CodeSnippetQualifiedNameReference(sources, positions,
							nameReference.sourceStart, nameReference.sourceEnd, evaluationContext);
				} else {
					assert kind == ASTNodeKind.NORMAL;

					return new QualifiedNameReference(
							sources, positions, nameReference.sourceStart, nameReference.sourceEnd);
				}
			}
		} else {
			SingleNameReference singleNameReference = (SingleNameReference) nameReference;
			result = new SingleNameReference(singleNameReference.token,
					EclipseProcessorUtils.encodePos(singleNameReference.sourceStart, singleNameReference.sourceEnd));
		}

		copyProperties(nameReference, result);
		
		return result;
	}
	
	public static void restoreNameReferences(CompilationUnitDeclaration compilationUnit,
			final Map<NameReference, NameReference> backuppedNameReferences) {
		compilationUnit.traverse(new NameReferenceVisitor() {
		
			@Override
			public void visitNameReference(NameReference nameReference) {
				NameReference backuppedNameReference = (NameReference) backuppedNameReferences.get(nameReference);

				if (((nameReference instanceof SingleNameReference) && ((backuppedNameReference == null)
								|| !Arrays.equals(((SingleNameReference) nameReference).token,
										((SingleNameReference) backuppedNameReference).token)))
						|| ((nameReference instanceof QualifiedNameReference)
								&& ((backuppedNameReference == null)
									|| !CharOperation.equals(((QualifiedNameReference) nameReference).tokens,
											((QualifiedNameReference) backuppedNameReference).tokens)))) {
					return;
				}

				copyProperties(backuppedNameReference, nameReference);
			}
		}, compilationUnit.scope);
	}	

	private static void copyProperties(NameReference source, NameReference target) {
		// fields from ASTNode
		target.sourceStart = source.sourceStart;
		target.sourceEnd = source.sourceEnd;
		target.bits = source.bits;
		if ((target.bits & ASTNode.RestrictiveFlagMASK) == Binding.FIELD
				|| (target.bits & ASTNode.RestrictiveFlagMASK) == Binding.LOCAL) {
			target.bits |= Binding.VARIABLE;
		}

		// fields from Expression
		target.constant = source.constant;
		target.statementEnd = source.statementEnd;
		target.implicitConversion = source.implicitConversion;
		target.resolvedType = source.resolvedType;

		// fields from NameReference
		target.binding = source.binding;
		target.actualReceiverType = source.actualReceiverType;

		if (source instanceof QualifiedNameReference) {
			assert target instanceof QualifiedNameReference;
			copyQualifiedNameReferenceSpecificProperties(
					(QualifiedNameReference) source, (QualifiedNameReference) target);
		} else {
			assert (source instanceof SingleNameReference) && (target instanceof SingleNameReference);

			copySingleNameReferenceSpecificProperties((SingleNameReference) source, (SingleNameReference) target);			
		}
	}

	private static void copyQualifiedNameReferenceSpecificProperties(
			QualifiedNameReference source, QualifiedNameReference target) {
		// ignore the 'token' field
		// ignore the 'tokens' field; MAYBE we SHOULD clone these?!?
		target.otherBindings = cloneArray(source.otherBindings);
		//TODO int[] otherDepths;
		target.indexOfFirstFieldBinding = source.indexOfFirstFieldBinding;
		target.syntheticWriteAccessor = source.syntheticWriteAccessor;
		target.syntheticReadAccessors = cloneArray(source.syntheticReadAccessors);
		target.genericCast = source.genericCast;
		target.otherGenericCasts = cloneArray(source.otherGenericCasts);

		if (target instanceof CompletionOnQualifiedNameReference) {
			((CompletionOnQualifiedNameReference) target).completionIdentifier =
				((CompletionOnQualifiedNameReference) source).completionIdentifier;
			((CompletionOnQualifiedNameReference) target).isInsideAnnotationAttribute =
				((CompletionOnQualifiedNameReference) source).isInsideAnnotationAttribute;
		}
	}

	private static void copySingleNameReferenceSpecificProperties(
			SingleNameReference source, SingleNameReference target) {
		// ignore the 'token' field
		target.syntheticAccessors = cloneArray(source.syntheticAccessors);
		target.genericCast = source.genericCast;
	}

	private static <T> T[] cloneArray(T[] array) {
		if (array == null) {
			return null;
		}
	
		return Arrays.copyOf(array, array.length);
	}	

	private static abstract class NameReferenceVisitor extends ASTVisitor {
		public void endVisit(QualifiedNameReference qualifiedNameReference, BlockScope scope) {
			visitNameReference(qualifiedNameReference);
		}

		public void endVisit(QualifiedNameReference qualifiedNameReference, ClassScope scope) {
			visitNameReference(qualifiedNameReference);
		}

		public void endVisit(SingleNameReference singleNameReference, BlockScope scope) {
			visitNameReference(singleNameReference);
		}

		public void endVisit(SingleNameReference singleNameReference, ClassScope scope) {
			visitNameReference(singleNameReference);
		}

		public abstract void visitNameReference(NameReference nameReference);	
	}	

	private NameReferenceUtils() {
		// Utility class
	}
}
